/*******************************************************************************
 * Copyright 2020-2023 Zebrunner Inc (https://www.zebrunner.com).
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *******************************************************************************/
package com.zebrunner.carina.core.testng;

import java.lang.invoke.MethodHandles;
import java.lang.reflect.Parameter;
import java.util.Arrays;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testng.ITestContext;
import org.testng.ITestNGMethod;
import org.testng.ITestResult;

import com.zebrunner.agent.testng.listener.RunContextService;
import com.zebrunner.carina.core.config.ReportConfiguration;
import com.zebrunner.carina.utils.ParameterGenerator;
import com.zebrunner.carina.utils.commons.SpecialKeywords;
import com.zebrunner.carina.utils.config.Configuration;

public class TestNamingService {
    private static final Logger LOGGER = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); 
    
    static final ThreadLocal<String> testName = new ThreadLocal<>();
    private static final ConcurrentHashMap<String, AtomicInteger> testNameInvCounter = new ConcurrentHashMap<>();

    /**
     * Get full test name based on test class, method and other generic information. It is generated by TestNameListener automatically.
     * 
     * @return String test name
     */
    @Deprecated
    public static String getTestName() {
        // TODO: think about returning very simple valid name if nothing was specified yet! Need ITestResult arg for that!
        if (testName.get() == null) {
            throw new RuntimeException("Unable to detect full test name yet!");
        }
        return testName.get();
    }
    
    /**
     * Get full test name based on test class, method and other generic information. It is generated by TestNameListener automatically.
     * 
     * @param result ITestResult
     * @return String test name
     */    
    public static String getTestName(ITestResult result) {
        // do not return name by thread because it is external Zafira call which should override all values
        setTestName(result);
        return testName.get();
    }
    
    /**
     * Set any custom full test name.
     * 
     * @param name String
     * @return String test name
     */ 
    public static String setTestName(String name) {
        LOGGER.warn("Overridden testName: {}", name);
        testName.set(name);
        return testName.get();
    }

    
    /**
     * Set full test name based on test class, method and other generic information. It is generated based by ITestResult object.
     * 
     * @param result ITestResult
     * @return String test name
     */     
    @SuppressWarnings("unlikely-arg-type")
    private static String setTestName(ITestResult result) {
        String name = StringUtils.EMPTY;
        if (result.getTestContext() == null) {
            throw new RuntimeException("Unable to set Test name without testContext!");
        }

        ITestNGMethod method = result.getMethod();

        name = Configuration.getRequired(ReportConfiguration.Parameter.TEST_NAMING_PATTERN);
        name = name.replace(SpecialKeywords.TEST_NAME, getTestNameMap(result))
                .replace(SpecialKeywords.TEST_NAME_TUID, getMethodUID(result))
                .replace(SpecialKeywords.METHOD_NAME, method.getMethodName())
                .replace(SpecialKeywords.METHOD_PRIORITY, String.valueOf(method.getPriority()))
                .replace(SpecialKeywords.METHOD_THREAD_POOL_SIZE, String.valueOf(method.getThreadPoolSize()))
                .replace(SpecialKeywords.METHOD_GROUP_NAMES, String.join(", ", method.getGroups()))
                .replace(SpecialKeywords.METHOD_DESCRIPTION, String.valueOf(method.getDescription()))
                .replace(SpecialKeywords.TEST_NAME_CLASS, method.getTestClass().getRealClass().getSimpleName())
                .trim();

        LOGGER.debug("testName: {}", name);

        // introduce invocation count calculation here as in multi threading mode TestNG doesn't provide valid
        // getInvocationCount() value
        name = appendDataProviderLine(result, name);
        name = appendInvocationCount(result, name);

        testName.set(name);
        return testName.get();
    }

    private static String getTestNameMap(ITestResult result) {
        String testNameMapName = result.getTestContext().getCurrentXmlTest().getName();

        @SuppressWarnings("unchecked")
        Map<Object[], String> testNameMap = (Map<Object[], String>) result.getTestContext().getAttribute(SpecialKeywords.TEST_NAME);
        if (testNameMap != null && !testNameMap.isEmpty()) {
            String testHash = ParameterGenerator.hash(result.getParameters(), result.getMethod());
            if (testNameMap.containsKey(testHash)) {
                testNameMapName = testNameMap.get(testHash);
            }
        }
        return testNameMapName;
    }

    private static String getMethodUID(ITestResult result) {
        String methodUID = StringUtils.EMPTY;

        try {
            ITestNGMethod testNGMethod = result.getMethod();

            Parameter[] parameters = result.getTestClass()
                    .getRealClass()
                    .getMethod(testNGMethod.getMethodName(),
                            testNGMethod.getParameterTypes())
                    .getParameters();

            for (int i = 0; i < parameters.length; i++) {
                if (parameters[i].getName().equalsIgnoreCase(SpecialKeywords.TUID)) {
                    // AUTO-274 "Pass"ing status set on emailable report when a test step fails
                    if (result.getParameters()[i] == null) {
                        break;
                    }

                    methodUID = result.getParameters()[i].toString();
                    if (methodUID.contains(SpecialKeywords.TUID + ":")) {
                        methodUID = methodUID.replace(SpecialKeywords.TUID + ":", "");
                    }
                    break;
                }
            }
        } catch (NoSuchMethodException e) {
            LOGGER.error("For some reason test method not found using reflection: {}", result.getMethod().getMethodName());
        }

        @SuppressWarnings("unchecked")
        Map<Object[], String> tuidMap = (Map<Object[], String>) result.getTestContext().getAttribute(SpecialKeywords.TUID);

        if (tuidMap != null) {
            String testHash = ParameterGenerator.hash(result.getParameters(), result.getMethod());
            if (tuidMap.containsKey(testHash)) {
                methodUID = tuidMap.get(testHash);
            }
        }

        return methodUID;
    }

    /**
     * get Test Method name
     * 
     * @param result ITestResult
     * @return String method name
     */
    @Deprecated(since = "8.0.1", forRemoval = true)
    public static String getMethodName(ITestResult result) {
        // adjust testName using pattern
        ITestNGMethod m = result.getMethod();
        String name = Configuration.getRequired(ReportConfiguration.Parameter.TEST_NAMING_PATTERN);
        LOGGER.debug("TestNamingPattern: {}", name);
        name = name.replace(SpecialKeywords.METHOD_NAME, m.getMethodName());
        name = name.replace(SpecialKeywords.METHOD_PRIORITY, String.valueOf(m.getPriority()));
        name = name.replace(SpecialKeywords.METHOD_THREAD_POOL_SIZE, String.valueOf(m.getThreadPoolSize()));
        name = name.replace(SpecialKeywords.METHOD_GROUP_NAMES, Arrays.toString(m.getGroups()));

        if (m.getDescription() != null) {
            LOGGER.debug("Test method description: {}", m.getDescription());
            name = name.replace(SpecialKeywords.METHOD_DESCRIPTION, m.getDescription());
        } else {
            name = name.replace(SpecialKeywords.METHOD_DESCRIPTION, "");
        }
        
        return name;
    }
    
    /**
     * get Test Package name
     * 
     * @param result ITestResult
     * @return String package name
     */
    public static String getPackageName(ITestResult result) {
        return result.getMethod().getRealClass().getPackage().getName();
    }
    
    /**
     * calculate InvocationCount number based on test name
     * 
     * @param testResult ITestResult
     * @return int invCount
     */
    private static String appendInvocationCount(ITestResult testResult, String testName) {
        int expectedInvocationCount = getInvocationCount(testResult);
        if (expectedInvocationCount > 1) {
            // adding extra zero at the beginning of the invocation count
            int indexMaxLength = Integer.toString(expectedInvocationCount).length() + 1;
            String lineFormat = " [InvCount=%0" + indexMaxLength + "d]";
            int currentInvocationCount = testNameInvCounter.computeIfAbsent(testName, $ -> new AtomicInteger(0))
                                                             .incrementAndGet();
            testName += String.format(lineFormat, currentInvocationCount);
        }
        return testName;
    }
    private static int getInvocationCount(ITestResult testResult) {
        ITestNGMethod[] methods = testResult.getTestContext().getAllTestMethods();
        return Arrays.stream(methods)
                     .filter(method -> method.equals(testResult.getMethod()))
                     .findFirst()
                     .map(ITestNGMethod::getInvocationCount)
                     .orElse(0);
    }
    
    private static String appendDataProviderLine(ITestResult testResult, String testName) {
        ITestNGMethod testMethod = testResult.getMethod();
        ITestContext testContext = testResult.getTestContext();
        Object[] parameters = testResult.getParameters();

        int dataProviderSize = RunContextService.getDataProviderSize(testMethod, testContext);

        if (dataProviderSize > 0) {
            // adding extra zero at the beginning of the data provider line number
            int indexMaxLength = Integer.toString(dataProviderSize).length() + 1;
            String lineFormat = " [L%0" + indexMaxLength + "d]";
            int index = RunContextService.getCurrentDataProviderIndex(testMethod, testContext, parameters) + 1;
            testName += String.format(lineFormat, index);
        }
        return testName;
    }
    
}
